/*
 * Package il.ac.biu.cs.grossmm.impl.activeData
 * File SqlRootMapping.java
 * 
 * This is a part of presence service framework prototype. 
 * See javadoc for more information.
 *  
 * Copyright (C) 2006 Grossmann Mark
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package il.ac.biu.cs.grossmm.impl.activeData;

import il.ac.biu.cs.grossmm.api.OperationFailedException;
import il.ac.biu.cs.grossmm.api.data.NodeType;
import il.ac.biu.cs.grossmm.api.keys.Key;
import il.ac.biu.cs.grossmm.api.keys.KeyPattern;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Set;
import java.util.WeakHashMap;

import org.apache.log4j.Logger;

public class SqlRootMapping<N> extends SqlMapping implements
		DataRetriever<Key, SqlRootNode<N>> {
	/**
	 * Logger for this class
	 */
	private static final Logger logger = Logger.getLogger(SqlRootMapping.class);

	final Discriminator discriminator;

	PreparedStatement insert;

	PreparedStatement delete;

	PreparedStatement selectIdByKey;

	Cache<Key, SqlRootNode<N>> rootCache;

	final WeakHashMap<Integer, SqlRootNode<N>> rootMap;

	SqlRootMapping(Connection connection, String tableNameBase,
			NodeType nodeType, KeyPattern pattern) throws SQLException {
		super(connection, tableNameBase, nodeType);
		this.discriminator = new Discriminator(pattern);

		this.rootMap = new WeakHashMap<Integer, SqlRootNode<N>>();
	}

	void prepare(Cache<SqlNodeBase, Object[]> valuesCache,
			Cache<SqlNodeSet, Set<SqlInnerNode>> subnodesCache,
			int rootCacheSize) throws SQLException {
		super.prepare(valuesCache, subnodesCache);

		this.rootCache = new Cache<Key, SqlRootNode<N>>(this, rootCacheSize);

		for (SqlInnerMapping sim : nodeSetMaps)
			sim.prepare(valuesCache, subnodesCache);

		// //////////// selectIdByKey
		// SELECT id FROM <table> WHERE key_1=? AND ... AND key_n=?

		StringBuffer sb = new StringBuffer("SELECT id FROM ");
		sb.append(tableName);
		sb.append(" WHERE ");

		for (int i = 0; i < discriminator.size(); i++) {
			if (i > 0)
				sb.append(" AND ");
			sb.append("key_").append(i).append("=?");
		}

		selectIdByKey = connection.prepareStatement(sb.toString());

		// //////////// insert
		// INSERT INTO <table> (is_visible, key_1,...,key_n)
		// VALUES (false, ?, ..., ?)

		sb.setLength(0);
		sb.append("INSERT INTO ").append(tableName).append(" (is_visible");
		for (int i = 0; i < discriminator.size(); i++) {
			sb.append(", key_").append(i);
		}

		sb.append(") VALUES (false");

		for (int i = 1; i <= discriminator.size(); i++) {
			sb.append(", ?");
		}

		sb.append(")");

		insert = connection.prepareStatement(sb.toString());

		// //////////// delete
		// DELETE FROM <table> WHERE key_1=? AND ... AND key_n=?

		sb.setLength(0);
		sb.append("DELETE FROM ").append(tableName).append(" WHERE ");
		for (int i = 0; i < discriminator.size(); i++) {
			if (i > 0)
				sb.append(" AND ");
			sb.append("key_").append(i).append("=?");
		}

		delete = connection.prepareStatement(sb.toString());
	}

	synchronized SqlRootNode<N> rootNode(int id, boolean create) {
		SqlRootNode<N> root;

		root = rootMap.get(id);

		if (create && root == null) {
			root = new SqlRootNode<N>(this, id);
			rootMap.put(id, root);
		}

		return root;
	}

	/**
	 * Create tables to serialize root node and subnodes data. Note that no
	 * commit is issued within this method.
	 * 
	 * @throws Exception
	 */
	void createTables() throws Exception {
		if (logger.isDebugEnabled()) {
			logger.debug("createRootTable() - start");
		}

		StringBuffer sb = new StringBuffer("CREATE TABLE ");
		sb.append(tableName);
		sb.append(" (id INTEGER IDENTITY, is_visible BOOLEAN");

		int i = 0;

		for (Discriminator.Entry e : discriminator.getEntries()) {
			String dbType = e.getDbType();
			if (dbType == null)
				throw new Exception("Cannot map " + e.getClass().getName()
						+ " to SQL type");

			sb.append(", ");
			sb.append("key_").append(i);
			sb.append(" ").append(dbType);

			i++;
		}

		insertFieldDefinitions(sb, propertyMaps);

		sb.append(")");

		String sqlCreateNodes = sb.toString();

		try {
			update("DROP TABLE " + tableName + " IF EXISTS");
			update(sqlCreateNodes);
			SqlInnerMapping[] subMaps = nodeSetMaps;

			for (i = 0; i < subMaps.length; i++) {
				SqlInnerMapping subMap = subMaps[i];
				subMap.createTables();
			}
		} catch (SQLException e) {
			logger.error(e);
			throw new Exception(
					"Could not create publication point (data nodes tables)");
		}

		if (logger.isDebugEnabled()) {
			logger.debug("createRootTable() - end");
		}
	}

	/**
	 * Drops all tables used by data nodes to serialize data. All data is lost.
	 * Note that no commit is issued within this method.
	 * 
	 * @throws SQLException
	 */
	void dropTables() throws SQLException {
		update("DROP TABLE " + tableName + " IF EXISTS");
		for (int i = 0; i < nodeSetMaps.length; i++) {
			SqlInnerMapping subMap = nodeSetMaps[i];
			subMap.dropTables();
		}
	}

	public synchronized SqlRootNode<N> create(Key key, boolean mayExist)
			throws SQLException, OperationFailedException {
		if (exists(key)) {
			if (mayExist)
				return get(key);
			else
				throw new OperationFailedException("Node already exists for "
						+ key);
		}

		// node does not exist

		discriminator.populate(key, insert, 1);

		insert.executeUpdate();

		// ResultSet rs = insert.executeQuery("CALL IDENTITY()");

		ResultSet rs = getLastId.executeQuery();

		rs.next();
		int id = rs.getInt(1);

		assert !rootMap.containsKey(id);
		assert !rootCache.isCached(key);

		return new SqlRootNode<N>(this, id);
	}

	public void delete(Key key) throws SQLException {
		int id = getIdByKey(key);

		// TODO use id in delete...
		discriminator.populate(key, delete, 1);

		delete.executeUpdate();

		rootMap.remove(id);
		rootCache.remove(key);

		// TODO delete subnodes (manually or ensure cascading)
	}

	public SqlRootNode<N> get(Key key) throws OperationFailedException {
		return rootCache.get(key);
	}

	public SqlRootNode<N> selectRoot(Key key) throws SQLException {
		int id = getIdByKey(key);

		return rootNode(id, true);
	}

	/**
	 * @param key
	 * @return
	 * @throws SQLException
	 */
	private int getIdByKey(Key key) throws SQLException {
		discriminator.populate(key, selectIdByKey, 1);

		ResultSet rs = selectIdByKey.executeQuery();

		if (!rs.next())
			throw new SQLException("Node does not exist for " + key);

		int id = rs.getInt(1);
		return id;
	}

	private boolean exists(Key key) throws SQLException {
		discriminator.populate(key, selectIdByKey, 1);

		ResultSet rs = selectIdByKey.executeQuery();

		return rs.next();
	}

	public SqlRootNode<N> retrieve(Key key) throws OperationFailedException {
		try {
			return selectRoot(key);
		} catch (SQLException e) {
			throw new OperationFailedException(e);
		}
	}
}
